﻿using System;
using System.Linq;
using System.ServiceModel;
using System.Text;

namespace SharePointUtilitiesForDevelopers.Service
{
    /// <summary>
    /// Provides constants and operations on SharePoint service urls.
    /// </summary>
    public static class SharePointServiceUrl
    {
        #region Constants

        /// <summary>
        /// The url of the owssvr dll.
        /// </summary>
        public const string OwsSvrUrl = "_vti_bin/owssvr.dll";

        /// <summary>
        /// The url of the SharePoint Lists service.
        /// </summary>
        public const string ListsServiceUrl = "_vti_bin/lists.asmx";

        /// <summary>
        /// The url of the SharePoint Views service.
        /// </summary>
        public const string ViewsServiceUrl = "_vti_bin/views.asmx";

        /// <summary>
        /// The url of the SharePoint SiteData service.
        /// </summary>
        public const string SiteDataServiceUrl = "_vti_bin/sitedata.asmx";

        /// <summary>
        /// The url of the SharePoint Webs service.
        /// </summary>
        public const string WebsServiceUrl = "_vti_bin/webs.asmx";

        /// <summary>
        /// The url of the SharePoint Sites service.
        /// </summary>
        public const string SitesServiceUrl = "_vti_bin/sites.asmx";

        /// <summary>
        /// The url of the SharePoint User Group service.
        /// </summary>
        public const string UserGroupServiceUrl = "_vti_bin/usergroup.asmx";

        #endregion Constants

        #region Methods

        /// <summary>
        /// Gets the service URI.
        /// </summary>
        /// <param name="siteUrl">The site URL.</param>
        /// <param name="serviceUrl">The service URL.</param>
        /// <returns>The url to the service.</returns>
        public static Uri GetServiceUri(string siteUrl, string serviceUrl)
        {
            // Return the service uri
            return new Uri(GetServiceUrl(siteUrl, serviceUrl));
        }

        /// <summary>
        /// Gets the service url.
        /// </summary>
        /// <param name="siteUrl">The site URL.</param>
        /// <param name="serviceUrl">The service URL.</param>
        /// <returns>The URI to the service.</returns>
        public static string GetServiceUrl(string siteUrl, string serviceUrl)
        {
            // Return the service url (site + service url)
            return siteUrl + (siteUrl.EndsWith("/") ? "" : "/") + serviceUrl;
        }

        /// <summary>
        /// Gets the service end point address.
        /// </summary>
        /// <param name="siteUrl">The site URL.</param>
        /// <param name="serviceUrl">The service URL.</param>
        /// <returns>The end point to the server.</returns>
        public static EndpointAddress GetServiceEndPointAddress(string siteUrl, string serviceUrl)
        {
            // Get the uri
            var serviceUri = GetServiceUri(siteUrl, serviceUrl);

            // Return the endpoint
            return new EndpointAddress(serviceUri);    
        }

        /// <summary>
        /// Url encodes the specified text.
        /// </summary>
        /// <param name="text">The text to encode.</param>
        /// <param name="additionalCharsToEncode">The additional characters to encode that otherwise wouldn't be encoded.</param>
        /// <returns>The encoded text.</returns>
        public static string UrlEncode(string text, IQueryable<char> additionalCharsToEncode)
        {
            // Get bytes
            var urlBytes = Encoding.UTF8.GetBytes(text);

            // Get the encoded bytes
            var encodedBytes = UrlEncode(urlBytes, 0, urlBytes.Length, additionalCharsToEncode);

            // Get ASCII string
            return Encoding.ASCII.GetString(encodedBytes);
        }

        /// <summary>
        /// Url encodes the specified text.
        /// </summary>
        /// <param name="bytes">The text bytes.</param>
        /// <param name="offset">The offset.</param>
        /// <param name="count">The count.</param>
        /// <param name="additionalCharsToEncode">The additional characters to encode that otherwise wouldn't be encoded.</param>
        /// <returns>The encoded text.</returns>
        private static byte[] UrlEncode(byte[] bytes, int offset, int count, IQueryable<char> additionalCharsToEncode)
        {
            var unsafeChars = 0;
            var spaceChars = 0;

            // Determine how many spaces and unsafe
            //  chars there are to encode
            for (var i = 0; i < count; i++)
            {
                var ch = (char)bytes[offset + i];
                if (ch == ' ')
                {
                    spaceChars++;
                }
                else if (!IsSafeUrlChar(ch) || additionalCharsToEncode.Contains(ch))
                {
                    unsafeChars++;
                }
            }

            // Nothing to do, return
            if ((unsafeChars == 0) && (spaceChars == 0))
            {
                return bytes;
            }

            // Create buffer to hold bytes plus space for encoded characters
            var buffer = new byte[count + (unsafeChars * 2)];
            var bufferPos = 0;

            for (var i = 0; i < count; i++)
            {
                var currentByte = bytes[offset + i];
                var currentChar = (char)currentByte;

                if (IsSafeUrlChar(currentChar) && !additionalCharsToEncode.Contains(currentChar))
                {
                    // Does not require encoding
                    buffer[bufferPos++] = currentByte;
                }
                else if (currentChar == ' ')
                {
                    // Encode space
                    buffer[bufferPos++] = 0x2b;
                }
                else
                {
                    // Requires encoding
                    buffer[bufferPos++] = 0x25; // %
                    buffer[bufferPos++] = (byte)IntToHex((currentByte >> 4) & 15);
                    buffer[bufferPos++] = (byte)IntToHex(currentByte & 15);
                }
            }
            return buffer;
        }

        /// <summary>
        /// Determines whether the specified character is safe in a url string.
        /// </summary>
        /// <param name="ch">The character.</param>
        /// <returns>
        /// 	<c>true</c> if the specified character is safe in a url string]; otherwise, <c>false</c>.
        /// </returns>
        private static bool IsSafeUrlChar(char ch)
        {
            // Letters and numbers are safe
            if ((((ch >= 'a') && (ch <= 'z')) || ((ch >= 'A') && (ch <= 'Z'))) || ((ch >= '0') && (ch <= '9')))
            {
                return true;
            }
            switch (ch)
            {
                case '\'':
                case '(':
                case ')':
                case '*':
                case '-':
                case '.':
                case '_':
                case '!':
                    return true; // certain special characters are safe
            }
            return false;
        }
        
        /// <summary>
        /// Converts the specified integer to hex.
        /// </summary>
        /// <param name="n">The integer.</param>
        /// <returns>A hex representation of the integer.</returns>
        private static char IntToHex(int n)
        {
            if (n <= 9)
            {
                return (char)(n + 0x30);
            }
            return (char)((n - 10) + 0x61);
        }
        
        #endregion Methods
    }
}